#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <string.h>
#include <ctype.h>
#include <fcntl.h>

#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define ARGC_SIZE 32
#define EXIT_CODE 44
#define LINE_SIZE 1024
#define DELIM " \t"

#define NONE -1
#define IN_RDIR     0
#define OUT_RDIR    1
#define APPEND_RDIR 2

int quit = 0;
int lastcode = 0;
char pwd[LINE_SIZE];
char commandline[LINE_SIZE];
char *argv[ARGC_SIZE];
extern char** environ;
char *rdirfilename = NULL;
int rdir = NONE;

// 模拟shell/bash的基本思路
// 1、获取命令行：命令行内容都来源于环境变量表
// 2、截取接收到的字符串
// 3、判断命令类型
//      如果是内建命令：需要根据每个命令的执行单独处理
//      如果是常规命令：父进程创建出子进程后，由子进程进行处理


// 自定义环境变量表：
char myenv[LINE_SIZE][LINE_SIZE];
int mysize = 0;

const char* getusername()
{
    return getenv("USER");
}
const char* gethostname()
{
    return getenv("HOSTNAME");
}
void getpwd()
{
    getcwd(pwd,sizeof(pwd));
}
void check_redir(char *cmd)
{
    char *pos = cmd;
    while(*pos)
    {
        if(*pos == '>')
        {
            // >>
            if(*(pos+1) == '>')
            {
                *pos++ = '\0';
                *pos++ = '\0';
                while(isspace(*pos))
                    pos++;
                rdirfilename = pos;
                rdir = APPEND_RDIR;
                break;
            }
            // >
            else 
            {
                *pos++ = '\0';
                while(isspace(*pos))
                    pos++;
                rdirfilename = pos;
                rdir = OUT_RDIR;
                break;
            }
        }
        // <
        else if(*pos == '<')
        {
            *pos++ = '\0';
            while(isspace(*pos))
                pos++;
            rdirfilename = pos;
            rdir = IN_RDIR;
            break;
        }
        else 
        {
            // nothing
        }
        pos++;
    }
}
// 获取命令行
void interact(char* cline,int size)
{
    getpwd();
    printf(LEFT"%s@%s %s"RIGHT""LABLE" ",getusername(),gethostname(),pwd);
    // 接收一行字符串
    char* s = fgets(cline,size,stdin);
    assert(s);
    // 设置结束符号，否则无法判断在哪里结束
    cline[strlen(cline)-1] = '\0';
    
    // ls -a -l > myfile.txt
    check_redir(cline);
}
// 截取字符串
int splitstring(char cline[],char* _argv[])
{
    int i = 0;
    // strtok：第一个参数不为NULL，函数将找到str中的第一个标记，strtop函数将保存它在字符串中的位置
    // 为NULL，函数将在同一个字符串中被保存的位置开始，查找下一个标记
    // 如果字符串中不存在更多的标记，则返回NULL指针
    argv[i] = strtok(cline,DELIM);
    while(argv[i++])
    {
        _argv[i] = strtok(NULL,DELIM);
    }
    
    // 返回截取了多少个字符串
    return i - 1;
}
// 常规命令由父进程分出的子进程执行，而父进程进行阻塞式等待(等待子进程执行结束，父进程再操作)
void NormalExcute(char* _argv[])
{
    pid_t id = fork();
    if(id < 0)
    {
        // 进程创建失败
        perror("fork");
        return;
    }
    else if(id == 0)
    {
        int fd = 0;
        
        // < 
        if(rdir == IN_RDIR)
        {
            fd = open(rdirfilename,O_RDONLY);
            dup2(fd,0);
        }
        // >
        else if(rdir == OUT_RDIR)
        {
            fd = open(rdirfilename,O_CREAT | O_WRONLY | O_TRUNC,0666);
            dup2(fd,1);
        }
        // >>
        else if(rdir == APPEND_RDIR)
        {
            fd = open(rdirfilename,O_CREAT | O_WRONLY | O_APPEND,0666);
            dup2(fd,1);
        }

        // 子进程
        // 子进程使用exec系列替换后面部分，并执行指令
        execvp(_argv[0],_argv);
        exit(EXIT_CODE);
    }
    else 
    {
        // 父进程
        int status = 0;

        // 阻塞式等待
        pid_t rid = waitpid(id,&status,0);
        if(rid == id)
        {
            lastcode = WEXITSTATUS(status);
        }
    }
}
// 内建命令需要单独判断并处理，因为内建命令是由父进程执行，常规命令由子进程执行
int buildCommand(char* _argv[],int _argc)
{
    if(_argc == 2 && strcmp(_argv[0],"cd") == 0)
    {
        // chdir：更改当前工作路径
        chdir(argv[1]);
        // 刷新pwd
        getpwd();
        // 将后两个参数的组合结果传入第一个参数
        // getenv("PWD")得到的结果是该程序环境变量的pwd
        // 所以是将当前程序环境变量的pwd进行更改
        sprintf(getenv("PWD"),"%s",pwd);
        return 1;
    }
    // export：导入环境变量
    else if(_argc == 2 && strcmp(_argv[0],"export") == 0)
    {
        // 系统的环境变量表内部存放的是环境变量字符串的地址
        // 所以如果更改了环境变量表内部对应地址的字符串，则会相应覆盖式的更改该环境变量
        // 而在这里存放的是字符串
        strcpy(myenv[mysize],_argv[1]);
        // putenv：给当前程序添加环境变量
        putenv((char*)myenv[mysize++]);
        return 1;
    }
    // echo：输出指定的环境变量内容、输出最近进程的状态码或输出字符串
    else if(_argc == 2 && strcmp(_argv[0],"echo") == 0)
    {
        // 输出最近进程结束的状态码
        if(strcmp(_argv[1],"$?") == 0)
        {
            printf("%d\n",lastcode);
            lastcode = 0;
        }
        // 输出$后的环境变量内容
        else if(*_argv[1] == '$')
        {
            char* val = getenv(_argv[1] + 1);
            if(val)
                printf("%s\n",val);
        }
        // 输出字符串
        else 
        {
            printf("%s\n",_argv[1]);
        }
        return 1;
    }
    
    // 特殊处理ls
    if(strcmp(_argv[0],"ls") == 0)
    {
        _argv[_argc++] = "--color";
        _argv[_argc] = NULL;
    }
    return 0;
}
int main()
{
    while(!quit)
    {
        // 每次运行前重置文件指向和状态标记
        rdirfilename = NULL;
        rdir = NONE;

        // 1.交互问题，获取命令行
        interact(commandline,sizeof(commandline));

        // 2.子串分割的问题，解析命令行
        int argc = splitstring(commandline,argv);
        // 如果是空串(即只有空格、\t和\n)，结束本次循环，开始下次循环
        if(argc <= 0) continue;

        // 3.指令的判断
        // 内建命令，本质就是一个shell内部的一个函数
        int n = buildCommand(argv,argc);

        // 4.0 分析输入的命令行字符串，获取有多少个|，命令打散多个子命令字符串
	// 4.1 malloc申请空间，pipe先申请多个管道
	// 4.2 循环创建多个子进程，每一个子进程的重定向情况，最开始，输出重定向，1->指定的一个管道的写端
	// 中间：输入输出重定向，0标准输入重定向到上一个管道的读端，1标准输出重定向到下一个管道的写端
	// 最后一个：输入重定向，将标准输入重定向到最后一个管道的读端
	// 4.3 分别让不同的子进程执行不同的命令 —— exec* —— 不会影响该进程曾经打开的文件，不会影响预先设置好的管道重定向

	// 5.普通命令的执行
	// 
        // 如果n是非假，说明该指令是普通命令，则由子进程执行
        if(!n)
            NormalExcute(argv);
    }

    return 0;
}
